package yawning.mew.sim;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.codehaus.commons.compiler.CompilerFactoryFactory;
import org.codehaus.commons.compiler.IClassBodyEvaluator;

import yawning.mew.MewView;
import yawning.mew.ProcEffect;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.ProcEffect.Trigger;
import yawning.mew.PropertiesBase;
import yawning.mew.Report;
import yawning.mew.character.Toon;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.Strategy.IAction;
import yawning.mew.sim.Strategy.IStatus;
import ec.util.MersenneTwisterFast;

public final class SimulationEngine {
	private static Map<String,WeakReference<Strategy>> sCache = new HashMap<String,WeakReference<Strategy>>();

	private Simulation mSim;
	private Toon mToon;
	private PropertiesBase mConfig;
	private MewView mView;
	private MersenneTwisterFast mPrng;
	private Strategy mStrategy;
	private double mEndTime;
	private double mSimTime;
	private double mGCDTime;
	private double mLastEventTime;
	private HashMap<IAction,SimulationAction> mActions = new HashMap<IAction,SimulationAction>();
	private HashMap<IAction,Double> mWatchDog = new HashMap<IAction,Double>();
	private List<SimulationProc> simProcs = new LinkedList<SimulationProc>();
	private Map<Trigger, List<SimulationProc>> simTriggers;

	int mEventHashCode = 0;
	TreeSet<SimulationEvent> mEventQueue = new TreeSet<SimulationEvent>();
	HashMap<Effect,EffectCallback> mEffectCallbacks = new HashMap<Effect,EffectCallback>();

	SimulationEngine(Simulation sim, Toon toon, PropertiesBase config, MewView view, int seed) {
		mSim = sim;
		mToon = toon;
		mConfig = config;
		mView = view;
		mPrng = new MersenneTwisterFast(seed);
		mEndTime = config.getDouble(Property.SIMULATION_DURATION);
		double g = mPrng.nextGaussian();
		if (g > 0.0) mEndTime += g * config.getDouble(Property.SIMULATION_DURATION_STDDEV);
		if (g < 0.0) mEndTime /= (1 - g * config.getDouble(Property.SIMULATION_DURATION_STDDEV) / mEndTime);

		String strat = config.getString(Property.SIMULATION_STRATEGY);
		if (strat.length() > 0) setStrategy(strat);
	}

	public final PropertiesBase getConfig() {
		return mConfig;
	}

	public final Toon getToon() {
		return mToon;
	}

	public final MewView getView() {
		return mView;
	}

	public final MersenneTwisterFast getRandom() {
		return mPrng;
	}

	public final double getEndTime() {
		return mEndTime;
	}

	public final boolean hasStrategy() {
		return mStrategy != null;
	}

	public final void setStrategy(Strategy strat) {
		mStrategy = strat;
	}

	public final void setStrategy(InputStream stream) {
		// Load the default strategy, if any.  Throw a runtime exception if there isn't one.
		StringBuilder sb = new StringBuilder();
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

			try {
				String line = null;
				while ((line = reader.readLine()) != null)
					sb.append(line).append('\n');
			} finally {
				reader.close();
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		setStrategy(sb.toString());
	}

	private final void setStrategy(String strat) {
		synchronized (sCache) {
			if (sCache.containsKey(strat)) mStrategy = sCache.get(strat).get();

			if (mStrategy == null) {
				StringReader sr = new StringReader(strat);
				try {
					IClassBodyEvaluator cbe = CompilerFactoryFactory.getDefaultCompilerFactory().newClassBodyEvaluator();
					cbe.setImplementedInterfaces(new Class[] { Strategy.class });
					Class<?>[] classes = mSim.getExportClasses();
					String[] classStrings = new String[classes.length];
					for (int i = 0; i < classes.length; i++) classStrings[i] = classes[i].getCanonicalName();
					cbe.setDefaultImports(classStrings);
					mStrategy = (Strategy) cbe.createInstance(sr);
					sCache.put(strat, new WeakReference<Strategy>(mStrategy));
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
	}

	public final void addAction(IAction action, SimulationAction r) {
		mActions.put(action, r);
	}

	public final double getSimTime() {
		return mSimTime;
	}

	public final Report run() {
		IStatus status = mSim.initSimulation(this);

		if (mStrategy == null) throw new RuntimeException("No valid strategies defined for Simulator!");

		mSimTime = mGCDTime = 0.0;
		for (SimulationEvent ev = mEventQueue.first(); (mSimTime = ev.mNextTickTime) < mEndTime; ev = mEventQueue.first()) {
			if (mSimTime >= mGCDTime) {
				if (mGCDTime >= mLastEventTime) mSimTime = mGCDTime;
				IAction action = mStrategy.getAction(status);
				if (action != null) {
					double gcd = mActions.get(action).perform();
					if (gcd == 0.0) {
						if (mWatchDog.containsKey(action) && (mWatchDog.get(action) == mSimTime)) {
							mView.onWarn("Watchdoged action " + action);
						}
						else {
							mWatchDog.put(action, mSimTime);
							continue;
						}
					}
					else if (gcd > 0.0) mGCDTime = mSimTime + gcd;
				}
			}
			ev = mEventQueue.pollFirst();
			mLastEventTime = mSimTime = ev.mNextTickTime;
			ev.onTick();
			if (mSimTime >= ev.mExpiryTime) {
				ev.mUp = false;
				ev.mUpTime += mSimTime - ev.getScheduleTime();
				ev.onExpire();
			}
			else {
				ev.mNextTickTime = mSimTime + ev.mTickInterval;
				mEventQueue.add(ev);
			}

			triggerProc(Trigger.USER);
		}
		mSimTime = mEndTime;

		return mSim.getReport(status);
	}

	public final List<SimulationProc> getProcs() {
		return simProcs;
	}

	public final void addProc(SimulationProc proc) {
		if (simTriggers == null) {
			simTriggers = new HashMap<Trigger, List<SimulationProc>>();
			for (ProcEffect.Trigger t : ProcEffect.Trigger.values()) simTriggers.put(t, new LinkedList<SimulationProc>());
		}

		simProcs.add(proc);
		proc.mSimUtil = this;
		for (Trigger t : proc.mEffect.triggers) {
			simTriggers.get(t).add(proc);
		}
	}

	public final void triggerProc(Trigger trigger) {
		if (simTriggers != null) for (SimulationProc proc : simTriggers.get(trigger)) proc.onProc();
	}

	public final void registerProcEffectCallback(Effect e, EffectCallback r) {
		mEffectCallbacks.put(e, r);
	}
}
